{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 数据操作\n",
    "\n",
    "为了开始我们的工作，首先我们需要存储和处理数据。通常，有两个重要的步骤：一，获取数据；二，得到数据后处理数据。如果不能存储数据，那么获取数据将没有意义。让我们从处理合成数据开始吧！首先，我们将介绍一个n-维数组（ndarray）, 它是 DJL 用来存储和转换数据的主要工具。在 DJL 中，`NDArray` 是一个类，任何示例被称为“an ndarray”。\n",
    "\n",
    "如果您使用过NumPy（Python中使用最广泛的科学计算包），那么您将对此章节非常熟悉。在 DJL 的 `NDArray` 不仅支持 CPU, 还支持GPU以及分离式云端结构。并且支持自动微分。在本书中，当提及ndarray，除非特别声明，都意味着是 DJL 的 `NDArray`。\n",
    "\n",
    "## 创建 `NDArray`\n",
    "\n",
    "在此小节，我们将带您上手，运行代码，为你准备好基础数学以及算数计算工具，使您通过本书步入学习正轨。不用为意会一些数学概念或者库的功能而烦恼。接下来的小结将会回顾在实际范例背景下的素材。另外，如果您已经具备相关背景，想要深入数学概念，请跳过本结。\n",
    "\n",
    "首先，我们需要倒入 DJL 的核心软件包以及常用类库，为了避免重复，我们把 maven 下载和常用类库引入的代码存到 `../utils/djl-import.ipynb` 文件中，这里我们只要使用 `%load` 宏调用即可。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%load ../utils/djl-imports"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "一个ndarray代表一个（可能是多维）含有数值的数组。一个有且只有一轴的ndarray在数学中对应一个向量，那么具有两轴的ndarray则对应一个矩阵。含有多于两轴但没有具体数学命名的数组，我们称之为*张量*，本书中我们统一使用 `NDArray`。\n",
    "\n",
    "新手上路，我们可以用arange创建一个行向量$\\vec{x}$，包含从0开始连续的12个整数。此处默认数据类型是浮点型。每一个数值都是一个ndarray，也是ndarray的*成员element*。例如，ndarray $\\vec{x}$中现在有12个*成员*。除非特殊声明，一个新的ndarray将被存储在主内存中，并且将进行基于CPU的相关计算。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "NDManager manager = NDManager.newBaseManager();\n",
    "NDArray x = manager.arange(12);\n",
    "x"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里我们将使用 [*NDManager*](https://javadoc.io/doc/ai.djl/api/latest/ai/djl/ndarray/NDManager.html)来创建ndarray$\\vec{x}$。*NDManager*执行界面[AutoClosable](https://docs.oracle.com/javase/8/docs/api/java/lang/AutoCloseable.html)并管理由它创建的ndarray的声明周期。因为Java Garbage Collector无法监管本地内存消耗，我们需要*NDManager*的帮助。通常我们会把NDManager封装在try blocks中，这样所有的ndarray可以被及时关掉。想要了解更多关于内存管理，请阅读DJL的[相关文档](https://github.com/awslabs/djl/blob/master/docs/development/memory_management.md)。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try(NDManager manager = NDManager.newBaseManager()){\n",
    "    NDArray x = manager.arange(12);\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们可以通过查看**shape**属性，获得 `NDArray` 的*shape维度*信息（每个轴的长度）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x.getShape()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "想要改变 `NDArray` 的维度并且不改变每个元素的数值，我们可以引用**reshape**功能。例如，我们可以这样转换我们的ndarray$\\vec{x}$，从维度(1, 12)的行向量转化成维度为(3, 4)的矩阵。这是一个新的ndarray，包含相同的数值但是是由3行和4列写成的。尽管*shape维度*改变了，但是$\\vec{x}$的成员没有改变。请注意，*size*不会因*reshape*而改变。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x = x.reshape(3, 4);\n",
    "x"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "手动明确每一个维度使用*reshape*，是非常繁琐的过程。如果我们的目标维度是一个带有具体*shape形状*矩阵，例如当我们已知的形状是用宽度来标记，那么高度信息就是隐含的已知条件。那么为什么我们要在此时再做一次除法运算？在上述的例子中，为了得到一个3行的矩阵，我们同时明确了该矩阵应该有3行和4列。现在，当已知其它的维度信息，ndarray可以自动计算。我们通过使用-1代替我们想要ndarray自动计算的维度。在DJL中，不用像使用x.reshape(3, 4)这样，x.reshape(-1, 4)或者x.reshape(3, -1)可以得到一样的结果。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "通过*create创建*的方法，只有*shape*会占用一些内存然后返回一个矩阵，此过程不会改变矩阵中任何数值。这是非常高效但我们也需要谨慎使用，因为矩阵的成员很有可能是任意数值，包括很大的任意数值。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "manager.create(new Shape(3, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "通常，我们希望矩阵初始化要不成员是0，要不是1或者其他常数，或者是明确维度分布的随机数。我们创建一个ndarray代表一个*tensor张量*，它的成员都是0，维度是(2, 3, 4)，我们这样做："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "manager.zeros(new Shape(2, 3, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "同样，我们可以创建一个成员都是1的*tensor张量*。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "manager.ones(new Shape(2, 3, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们更多希望从一些特定概率分布中的ndarrya为每一个成员采样随机样本值。例如，当我们在一个神经网络中创建多个array作为参数时，我们一般会用随机数将它们初始化。接下来的示例演示如何创建一个维度为(3, 4)的ndarray。它每一个成员将从高斯分布中随机提取。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "manager.randomNormal(0f, 1f, new Shape(3, 4), DataType.FLOAT32)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "你也可以直接使用*shape*，它会使用平均标准分布作为默认值。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "manager.randomNormal(new Shape(3, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们也可以通过按照需要构造ndarray，只需要明确每个成员的算数值和期望维度即可。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "manager.create(new float[]{2, 1, 4, 3, 1, 2, 3, 4, 4, 3, 2, 1}, new Shape(3, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 运算\n",
    "\n",
    "`NDArray` 支持大量的运算符（operator）。例如，我们可以对之前创建的两个形状为(3, 4)的 `NDArray` 做按元素加法。所得结果形状不变。\n",
    "\n",
    "因为Java不支持运算符过载, 在DJL中，常见的标准算数运算符（+，—，$*$，/和$**$）都通过函数来实现。可以对任意维度中任何同一维度的张量进行运算。我们可以对任意两个相同维度的张量进行基础运算。在接下来的例子中，我们用逗号构建一个含有5个元素的元组，其中每一个元素都是经过基础运算得到的结果。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "NDArray x = manager.create(new float[]{1f, 2f, 4f, 8f});\n",
    "NDArray y = manager.create(new float[]{2f, 2f, 2f, 2f});\n",
    "x.add(y);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x.sub(y);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x.mul(y);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x.div(y);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x.pow(y);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "DJL可以运行多种基础运算，比如一元操作符：指数函数。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x.exp()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "除了基础运算外，我们同样可以进行线性代数运算，包括向量点乘和矩阵相乘。稍后我们会在**sec_linear-algebra**解释线性代数的关键部分（无先学知识）。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们也可以将多个ndarrays *concatenate*联结起来，将它们首位相接地堆叠起来，生成一个更大的ndarray。我们只需要提供一个ndarray的列表然后告诉系统沿着哪个轴进行联结。下面的示例展示了当我们将两个矩阵分别沿0轴（行）和1轴（列）联结将会发生什么。我们可以看到第一种情况将输出ndarray的0轴长度为6，它是通过两个输入ndarray的0轴长度之和（3+3）得到的。与此同时，第二种情况将输出两个输入ndarray的1轴长度之和，4+4=8."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x = manager.arange(12f).reshape(3, 4);\n",
    "y = manager.create(new float[]{2, 1, 4, 3, 1, 2, 3, 4, 4, 3, 2, 1}, new Shape(3, 4));\n",
    "x.concat(y) // default axis = 0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x.concat(y, 1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "然而有时，我们想通过逻辑声明构建一个二元ndarray。比如，x.eq(y)。对于每一个定位，x和y的数值都相等，那么在相对应的位置上将生成一个新的ndarray，值为**1**，逻辑声明x.eq()表明在此定位为真。反之，此位置则为**0**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x.eq(y)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "对 `NDArray` 中的所有元素求和将生成一个只含有一个元素的 `NDArray`。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x.sum()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "读者可以使用像np.sum(x)的形式调用x.sum()\n",
    "\n",
    "\n",
    "## 广播机制\n",
    "\n",
    "在上面的章节，我们看到了基础运算符是如何在两个相同维度的ndarray中进行运算的。在不确定的条件下，甚至不同维度下，我们一样可以通过引进传播机制进行基础运算。此工作机制将按如下顺序被执行：首先，通过将每个元素正确地复制来扩展一个或同时两个数组。经过这样的变换，两个ndarray将拥有相同维度。接下来，在执行基础运算输出结果。\n",
    "\n",
    "在多数情况下，我们沿着一个初始长度为1的轴传播，像下面的例子一样："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "NDArray a = manager.arange(3f).reshape(3, 1);\n",
    "NDArray b = manager.arange(2f).reshape(1, 2);\n",
    "a"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "b"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "因为**a**和**b**分别是$3\\times 1$和$1\\times 2$的矩阵，它们不可以直接相加因为维度不匹配。所以我们将两个矩阵传播到一个更大的$3\\times 2$的矩阵中：在进行加法基础运算之前，复制矩阵**a**的列并复制矩阵**b**的行。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "a.add(b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 索引\n",
    "\n",
    "索引和切片功能中，DJL使用与Python中的Numpy相同的语法结构。就像在任意一个Python中的数组一样，一个ndarray中的元素也可以通过索引获得。像在任意Python的array一样，第一个元素的索引是**0**,明确范围包括第一个到最后一个元素之前。因为Python的标准列表中，我们可以通过负标记数，得到元素与列表末尾的相对位置，从而获得元素本身。\n",
    "\n",
    "因此，[-1]是最后一个元素。[1:3]表示第二个和第三个元素："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x.get(\":-1\");"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x.get(\"1:3\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们也可以根据标记数更改矩阵里的元素。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x.set(new NDIndex(\"1, 2\"), 9);\n",
    "x"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果我们想同时对多个元素赋相同的值，我们只要将它们一起索引然后赋值即可。比如，[0:2, :]获取第一至第二行，：表示沿着1轴（列）取所有元素。用索引的方法遍历矩阵，毫无疑问也可以遍历向量和张量。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x.set(new NDIndex(\"0:2, :\"), 12);\n",
    "x"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 运算的内存开销\n",
    "\n",
    "运行运算符需要主机分配新的内存空间。例如，如果我们写入y = x.add(y),我们将间接引用y之前指向的ndarray并在新分配的内存中替代y。\n",
    "\n",
    "这不是我们所期望的，原因如下：第一，我们不想总是在不必要的时候分配内存。在机器学习中，我们大概会有以兆字节为单位的参数，并且每秒中会多次更新它们。通常，我们只想它们在正确的时候运行更新。其次，多个变量会指向相同的参数。如果我们不能及时更新参数，其他间接引用的变量将会指向之前的内存地址，这样会使我们的部分代码有可能引用了之前的旧参数。\n",
    "\n",
    "幸运地是在DJL中，使用原位运算符很简单。我们可以用原位运算符如addi,subi,muli或divi，将分派运算结果到之前分配的数组中。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "NDArray original = manager.zeros(y.getShape());\n",
    "NDArray actual = original.addi(x);\n",
    "original == actual"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 小结\n",
    "\n",
    "* DJL中的ndarray是Numpy中ndarray的一种扩展，它具有一些强大功能使它更适合深度学习。\n",
    "* DJL中的ndarray提供多种多样的功能，其中包括基础数学运算，*broadcasting广播*，索引，切片，节省内存，转换其他Python对象。\n",
    "\n",
    "\n",
    "## 练习\n",
    "\n",
    "* 运行本节中的代码。将本节中条件判别式 `x.eq(y)` 改为 `x.lt(y)` 或`x.gt(y)`，看看能够得到什么样的 `NDArray`。\n",
    "* 将广播机制中按元素运算的两个 `NDArray` 替换成其他形状，结果是否和预期一样？\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Java",
   "language": "java",
   "name": "java"
  },
  "language_info": {
   "codemirror_mode": "java",
   "file_extension": ".jshell",
   "mimetype": "text/x-java-source",
   "name": "Java",
   "pygments_lexer": "java",
   "version": "14.0.2+12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
